﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;
using System.Security;
using System.Globalization;
using System.Collections.ObjectModel;

namespace Hallenberg.Core.IO
{
    /// <summary>
    /// This class encapsulates access to and from classic INI files.
    /// INI files are still widely used, however, not in the Core Windows System.
    /// Take a look at .URL files (your bookmarks). They are all INI files.
    /// </summary>
    public class IniFile
    {
        /// <summary>
        /// The internal field holding the ini file sections.
        /// </summary>
        private List<IniFileSection> m_IniFileSections;

        /// <summary>
        /// Gets or sets the file path if the INI file has been read or written.
        /// </summary>
        /// <value>The file path.</value>
        public string FilePath { get; set; }

        /// <summary>
        /// Gets the sections of the INI file.
        /// </summary>
        /// <value>The sections.</value>
        public ReadOnlyCollection<IniFileSection> Sections
        {
            get
            {
                return new ReadOnlyCollection<IniFileSection>(m_IniFileSections);
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="IniFile"/> class.
        /// </summary>
        public IniFile()
        {
            this.FilePath = string.Empty;
            m_IniFileSections = new List<IniFileSection>();
        }

        /// <summary>
        /// Add a new section to the ini file.
        /// </summary>
        /// <param name="section">Must be a valid, not-null object.</param>
        public void AddSection(IniFileSection section)
        {
            Debug.Assert(section != null);
            if (section == null)
            {
                throw new ArgumentNullException("section");
            }

            m_IniFileSections.Add(section);
        }

        /// <summary>
        /// Reads the specified file path and returns an instantiated IniFile class.
        /// Reading each section of the file is the responsibility of the IniFileSection class's Read method.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        /// <returns>A new instance of the IniFile or null if an exception occured.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown if filePath is null or empty.</exception>
        /// <exception cref="System.FileNotFoundException">Thrown if file cannot be found.</exception>
        /// <exception cref="System.DirectoryNotFoundException">Thrown if directory structure does not exist.</exception>
        /// <exception cref="IOException">Thrown if directory structure or path is otherwise invalid.</exception>
        public static IniFile Read(string filePath)
        {
            Debug.Assert(!string.IsNullOrEmpty(filePath));
            
            if (string.IsNullOrEmpty(filePath))
            {
                throw new ArgumentNullException("filePath");
            }

            TextReader tr = null;

            try
            {
                tr = new StreamReader(filePath);

                IniFile iniFile = new IniFile();

                string nextLine;
                IniFileSection newSection = null;

                while ((nextLine = tr.ReadLine()) != null)
                {
                    nextLine = nextLine.Trim();

                    if (string.IsNullOrEmpty(nextLine))
                    {
                        if (newSection != null)
                        {
                            iniFile.AddSection(newSection);
                            newSection = null;
                        }

                        continue;
                    }

                    if (nextLine.StartsWith(IniFileSection.SectionStart, StringComparison.OrdinalIgnoreCase) && nextLine.EndsWith(IniFileSection.SectionEnd, StringComparison.OrdinalIgnoreCase))
                    {
                        if (newSection != null)
                        {
                            iniFile.AddSection(newSection);
                        }

                        newSection = new IniFileSection(nextLine);
                        continue;
                    }

                    if (nextLine.Contains(IniFileSection.AssignmentOperator))
                    {
                        int index = nextLine.IndexOf(IniFileSection.AssignmentOperator, StringComparison.OrdinalIgnoreCase);

                        Debug.Assert(index != -1);
                        if (index == -1)
                        {
                            throw new InvalidOperationException("Values in section " + newSection.SectionName + " are not in the correct formate (Key=Value).");
                        }

                        newSection.Values[nextLine.Substring(0, index)] = nextLine.Substring(index + 1, nextLine.Length - index - 1);
                    }
                }

                if (newSection != null)
                {
                    iniFile.AddSection(newSection);
                }

                iniFile.FilePath = filePath;
                return iniFile;
            }
            catch (FileNotFoundException)
            {
                Debug.Assert(false, "File not found");
                throw;
            }
            catch (DirectoryNotFoundException)
            {
                Debug.Assert(false, "Directory not found");
                throw;
            }
            catch (IOException)
            {
                Debug.Assert(false, "IO exception occured");
                throw;
            }
            finally
            {
                if (tr != null)
                {
                    tr.Close();
                }
            }
        }

        /// <summary>
        /// Writes the specified INI file to path.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        /// <exception cref="System.ArgumentNullException">Thrown if filePath is null or empty ("").</exception>
        public void Write(string filePath)
        {
            Debug.Assert(!string.IsNullOrEmpty(filePath));

            if (string.IsNullOrEmpty(filePath))
            {
                throw new ArgumentNullException("filePath");
            }

            TextWriter tw = new StreamWriter(filePath);

            try
            {
                foreach (IniFileSection section in this.Sections)
                {
                    tw.WriteLine(section.SectionName);

                    foreach (string valueName in section.Values.Keys)
                    {
                        tw.WriteLine(valueName + IniFileSection.AssignmentOperator + section.Values[valueName]);
                    }

                    tw.WriteLine();
                }

                this.FilePath = filePath;
            }
            catch (UnauthorizedAccessException)
            {
                Debug.Assert(false, "Unauthorize access");
                throw;
            }
            catch (DirectoryNotFoundException)
            {
                Debug.Assert(false, "Directory not found");
                throw;
            }
            catch (PathTooLongException)
            {
                Debug.Assert(false, "Path too long");
                throw;
            }
            catch (IOException)
            {
                Debug.Assert(false, "Malformed path");
                throw;
            }
            catch (SecurityException)
            {
                Debug.Assert(false, "Not sufficient rights");
                throw;
            }
            finally
            {
                if (tw != null)
                {
                    tw.Flush();
                    tw.Close();
                }
            }
        }
    }

}
